package com.mercycrazy.recruit.service.impl;

import cn.hutool.core.util.DesensitizedUtil;
import com.alibaba.fastjson.JSON;
import com.mercycrazy.recruit.dao.AdminDao;
import com.mercycrazy.recruit.dao.UserDao;
import com.mercycrazy.recruit.entity.Admin;
import com.mercycrazy.recruit.entity.User;
import com.mercycrazy.recruit.enums.Role;
import com.mercycrazy.recruit.enums.Sex;
import com.mercycrazy.recruit.enums.ShowWhichName;
import com.mercycrazy.recruit.enums.StatusCode;
import com.mercycrazy.recruit.exception.BusinessException;
import com.mercycrazy.recruit.filter.AccessContext;
import com.mercycrazy.recruit.service.IUserAuthService;
import com.mercycrazy.recruit.user.*;
import com.mercycrazy.recruit.util.IdUtil;
import com.mercycrazy.recruit.util.MD5Util;
import com.mercycrazy.recruit.util.RandomUtil;
import com.mercycrazy.recruit.util.jwt.JWTProperties;
import com.mercycrazy.recruit.util.jwt.JWTUtil;
import com.mercycrazy.recruit.vo.mq.SendSMS;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

import static com.mercycrazy.recruit.constant.CommonConstant.*;
import static com.mercycrazy.recruit.constant.RabbitMQConstant.SMS_EXCHANGE;
import static com.mercycrazy.recruit.constant.RabbitMQConstant.SMS_LOGIN_SEND_ROUTING;
import static com.mercycrazy.recruit.constant.RedisConstant.*;
import static com.mercycrazy.recruit.constant.UserConstant.*;

/**
 * 鉴权微服务相关服务接口实现
 *
 * @author <a href="mailto:1443424326@qq.com">mercycrazy</a>
 * @since 2023-01-14
 */
@Slf4j
@Service
public class UserAuthServiceImpl implements IUserAuthService {

    private final RabbitTemplate rabbitTemplate;

    private final StringRedisTemplate redisTemplate;

    private final UserDao userDao;

    private final AdminDao adminDao;

    private final JWTProperties jwtProperties;

    public UserAuthServiceImpl(RabbitTemplate rabbitTemplate,
                               StringRedisTemplate redisTemplate,
                               UserDao userDao,
                               AdminDao adminDao, JWTProperties jwtProperties) {
        this.rabbitTemplate = rabbitTemplate;
        this.redisTemplate = redisTemplate;
        this.userDao = userDao;
        this.adminDao = adminDao;
        this.jwtProperties = jwtProperties;
    }

    @Override
    public void sendSMSCode(String mobile, long validTime) {

        // 1. 手机号校验
        if (StringUtils.isBlank(mobile)) {
            log.error("sendSMSCode request error: mobile is empty");
            BusinessException.display(StatusCode.USER_PARAMS_ERROR);
        }

        // 2. 判断验证码 60s 是否已经发送过, 基于 redis value 拼接时间戳方案
        String redisKey = String.format(MOBILE_SMS_CODE, mobile);
        String redisCode = redisTemplate.opsForValue().get(redisKey);

        if (StringUtils.isNotBlank(redisCode)) {
            long ttl = Long.parseLong(redisCode.split("_")[1]);
            long duration = System.currentTimeMillis() - ttl;
            // 当前时间戳 - 验证码发送时间戳, 如果小于60秒, 则不给重复发送
            if (duration < 1000 * 60) {
                log.error("重复发送验证码, 时间间隔: {}秒", duration / 1000);
                BusinessException.display(StatusCode.SMS_NEED_WAIT_ERROR);
            }
        }

        // 3. 发送短信验证码
        String code = RandomUtil.getRandomCode(6);
        log.info("send sms code, mobile: [{}], code: [{}]", mobile, code);
        SendSMS sendSMS = new SendSMS(mobile, code, validTime);

        // 消息可靠性保证(一) - confirmCallback 投递到交换机后执行回调函数
        // correlationData 相关性数据
        // ack             交换机是否成功接收到消息, true: 成功
        // cause           失败的原因
        rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
            if (!ack) {
                // TODO 消息发送失败的处理
            }
        });
        // 消息可靠性保证(二) - returnCallback 交换机路由消息到队列失败执行的回调
        rabbitTemplate.setReturnsCallback(returned -> {
            // TODO 消息路由失败的处理
        });
        rabbitTemplate.convertAndSend(
                SMS_EXCHANGE,
                SMS_LOGIN_SEND_ROUTING,
                JSON.toJSONString(sendSMS),
                // 消息属性处理的类对象 (对当前需要的超时 ttl 进行参数属性的设置)
                // 如果消息和队列都设置了 ttl, 那么哪个小哪个生效
                message -> {
                    message.getMessageProperties()
                            .setExpiration(String.valueOf(20 * 1000));
                    return message;
                }
        );

        // 4. 手机号验证码 30 分钟有效, 验证码拼接时间戳, 存入 redis
        redisCode = code + "_" + System.currentTimeMillis();
        redisTemplate.opsForValue().set(redisKey, redisCode, validTime, TimeUnit.MINUTES);
    }

    @Override
    public UserInfo registerUserByMobile(String mobile) {

        User user = new User();

        user.setMobile(mobile);
        user.setNickname("用户" + DesensitizedUtil.mobilePhone(mobile));
        user.setRealName("用户" + DesensitizedUtil.mobilePhone(mobile));
        user.setShowWhichName(ShowWhichName.NICKNAME);
        user.setSex(Sex.SECRET);
        user.setFace(DEFAULT_USER_FACE);
        user.setEmail("");
        user.setBirthday(DEFAULT_BIRTHDAY);
        user.setCountry(DEFAULT_COUNTRY);
        user.setProvince("");
        user.setCity("");
        user.setDistrict("");
        user.setDescription(DEFAULT_DESCRIPTION);
        user.setPosition(DEFAULT_POSITION);
        user.setRole(Role.CANDIDATE);

        user = userDao.save(user);

        return user.toUserInfo();
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public UserLoginInfo loginAndGenerateToken(MobileAndCode mobileAndCode) {

        String mobile = mobileAndCode.getMobile();
        String code = mobileAndCode.getCode();

        // 1. 根据手机号去 redis 中判断验证码是否存在, 如果存在, 则判断与传进来的验证码是否匹配
        String redisKey = String.format(MOBILE_SMS_CODE, mobile);
        String redisCode = redisTemplate.opsForValue().get(redisKey);
        if (StringUtils.isBlank(redisCode) ||
                !code.equalsIgnoreCase(redisCode.split("_")[0])
        ) {
            BusinessException.display(StatusCode.SMS_CODE_ERROR);
        }

        // 2. 判断该用户是否已经注册
        User user = userDao.findByMobile(mobile).orElse(null);
        UserInfo userInfo;
        if (null == user) {
            // 2.1 用户不存在, 走注册逻辑
            userInfo = registerUserByMobile(mobile);
            log.info("register new user: [{}]", JSON.toJSONString(userInfo));
        } else {
            // 2.2 用户存在, 直接返回登录信息
            userInfo = user.toUserInfo();
            log.info("user login success, mobile: [{}], [{}]", mobile, userInfo);
        }

        // 3. 生成 Jwt Token
        Map<String, String> jwtPayload = new HashMap<>();
        jwtPayload.put(JWT_USER_INFO_KEY, JSON.toJSONString(userInfo));

        String token = JWTUtil.generateTokenWithPrefix(
                jwtPayload, jwtProperties.getExpire(),
                jwtProperties.getSecretKey(), APP_JWT_PREFIX
        );
        log.info("generate jwt token for login user, mobile: [{}], token: [{}]",
                mobile, token);

        // 4. 删除 redis cache 中的验证码和手机号
        redisTemplate.delete(redisKey);

        // 5. 返回用户登录信息
        return new UserLoginInfo(userInfo, token);
    }

    @Override
    public String generateQrToken() {

        // 生成扫码登录的二维码 token
        String qrToken = IdUtil.generateUUID();

        log.info("generate qrToken: [{}]", qrToken);

        // qrToken 存入 redis, 5分钟有效, 过期需要重新生成
        // QR_TOKEN_UNREAD_STATUS 标记当前的 qrToken 未被扫描读取
        redisTemplate.opsForValue().set(
                String.format(SAAS_PLATFORM_LOGIN_QR_TOKEN, qrToken),
                QR_TOKEN_UNREAD_STATUS, 5, TimeUnit.MINUTES
        );

        // 返回给前端, 前端请求预登录令牌时需要带上 qrToken
        return qrToken;
    }

    @Override
    public String scanCode(String qrToken) {

        // 1. 校验 qrToken 有效性
        String redisQrTokenStatus = redisTemplate.opsForValue().get(
                String.format(SAAS_PLATFORM_LOGIN_QR_TOKEN, qrToken)
        );
        if (StringUtils.isBlank(redisQrTokenStatus) ||
                !redisQrTokenStatus.equals(QR_TOKEN_UNREAD_STATUS)) {

            log.error("qrToken is invalid: [{}]", qrToken);
            throw new BusinessException(StatusCode.FAILED);
        }

        // 2. 校验是否具有 HR 身份
        if (!Role.RECRUITER.getType().equals(
                AccessContext.getLoginUserInfo().getRole())) {

            log.error("user scan code without saas role, userId: [{}], qrToken:[{}]",
                    AccessContext.getLoginUserInfo().getId(), qrToken);
            throw new BusinessException(StatusCode.HR_TICKET_INVALID);
        }

        // 3. 生成预登录 token
        String preToken = IdUtil.generateUUID();

        // 4. 标记 redis qrToken 状态为已读, 将 preToken 写入 redis
        redisTemplate.opsForValue().set(
                String.format(SAAS_PLATFORM_LOGIN_QR_TOKEN, qrToken), QR_TOKEN_READ_STATUS,
                5, TimeUnit.MINUTES
        );
        redisTemplate.opsForValue().set(
                String.format(SAAS_PLATFORM_PRE_TOKEN, qrToken), preToken,
                5, TimeUnit.MINUTES
        );
        // 5. 返回 preToken, 手机端下次请求需要携带 preToken
        return preToken;
    }

    @Override
    public Boolean qrTokenHasBeenScan(String qrToken) {

        String qrTokenStatus = redisTemplate.opsForValue().get(
                String.format(SAAS_PLATFORM_LOGIN_QR_TOKEN, qrToken));

        if (StringUtils.isBlank(qrTokenStatus)) {
            throw new BusinessException(StatusCode.FAILED);
        }

        if (Objects.equals(qrTokenStatus, QR_TOKEN_READ_STATUS)) {
            log.info("qrToken has been read, delete from redis: [{}]", qrToken);
            redisTemplate.delete(
                    String.format(SAAS_PLATFORM_LOGIN_QR_TOKEN, qrToken));
            return Boolean.TRUE;
        }

        return Boolean.FALSE;
    }

    @Override
    public void confirmSaasLogin(String qrToken, String preToken) {

        String redisPreToken = redisTemplate.opsForValue().get(
                String.format(SAAS_PLATFORM_PRE_TOKEN, qrToken));

        if (!Objects.equals(redisPreToken, preToken)) {
            throw new BusinessException(StatusCode.FAILED);
        }

        UserInfo hrUserInfo = AccessContext.getLoginUserInfo();

        // 临时存入用户信息到 Redis 中, 以便于 PC 端通过 preToken 获取用户信息(PC 端获取不用用户 id, 只能通过该方式实现)
        // 如果使用 websocket, 则不需要通过该方式实现, 直接传 userId 到网页端即可
        log.info("cached userInfo to redis for saas login: [{}]", JSON.toJSONString(hrUserInfo));
        redisTemplate.opsForValue().set(
                String.format(SAAS_LOGIN_USER_INFO, qrToken),
                JSON.toJSONString(hrUserInfo), 5, TimeUnit.MINUTES
        );

        // 删除 redis 中关于 preToken 的缓存信息
        redisTemplate.delete(String.format(SAAS_PLATFORM_PRE_TOKEN, qrToken));
    }

    @Override
    public UserLoginInfo checkLogin(String qrToken) {

        // 获得用户临时信息
        String userJson = redisTemplate.opsForValue().get(
                String.format(SAAS_LOGIN_USER_INFO, qrToken)
        );

        if (StringUtils.isBlank(userJson)) {
            return null;
        }
        log.info("saas check login success, userInfo: [{}]", userJson);

        // 确认执行登录后, 生成 SAAS 用户的 token, 并且长期有效
        Map<String, String> jwtPayload = new HashMap<>();
        jwtPayload.put(JWT_USER_INFO_KEY, userJson);
        String saasUserToken = JWTUtil.generateTokenWithPrefix(
                jwtPayload, null,
                jwtProperties.getSecretKey(), SAAS_JWT_PREFIX
        );

        // 删除 redis 中缓存的临时用户信息
        redisTemplate.delete(String.format(SAAS_LOGIN_USER_INFO, qrToken));

        // 返回用户登录对象
        return new UserLoginInfo(
                JSON.parseObject(userJson, UserInfo.class), saasUserToken);
    }

    @Override
    public AdminLoginInfo adminLogin(UsernameAndPassword usernameAndPassword) {

        // 根据 username 查询用户
        Admin admin = adminDao.findFirstByUsername(
                usernameAndPassword.getUsername()).orElse(null);

        if (null == admin) {
            throw new BusinessException(StatusCode.ADMIN_LOGIN_ERROR);
        }

        // 获取到 salt
        String salt = admin.getSalt();

        // 明文密码 + 盐 => MD5 加密
        String md5Password = MD5Util.encrypt(
                usernameAndPassword.getPassword(), salt);

        if (!Objects.equals(md5Password, admin.getPassword())) {
            throw new BusinessException(StatusCode.ADMIN_LOGIN_ERROR);
        }

        SimpleAdminInfo adminInfo = admin.toSimple();

        // 生成 Admin JWT Token
        Map<String, String> jwtPayload = new HashMap<>();
        jwtPayload.put(JWT_USER_INFO_KEY, JSON.toJSONString(adminInfo));
        String adminToken = JWTUtil.generateTokenWithPrefix(
                jwtPayload, null,
                jwtProperties.getSecretKey(), ADMIN_JWT_PREFIX
        );

        log.info("admin login success, generate token for admin: [{}], [{}]",
                adminToken, JSON.toJSONString(adminInfo));

        return new AdminLoginInfo(adminInfo, adminToken);
    }
}
